// 1. 未调用 fcntl 或其他接口设置 O_NONBLOCK，因此 socket 处于默认阻塞模式。
// 2. accept 与 recv 均为阻塞调用：无连接/无数据时，线程睡眠等待，CPU资源得以释放。
// 3. 为了处理多个连接，示例使用“每连接一个线程”的经典模型，体现了阻塞模式的缺点：线程数量随连接数线性增长。

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>

#include <cstring>
#include <iostream>
#include <stdexcept>
#include <thread>

constexpr int kListenPort = 8080;
constexpr int kBacklog    = 16;
constexpr size_t kBufSize = 4096;

/**
 * 处理单个客户端连接的函数。
 * 这里特意不设置套接字为非阻塞模式，以保持“默认阻塞”语义。
 * 理由：
 *   1. 阻塞式 recv 更容易编写和理解，避免了轮询、状态机等额外复杂度。
 *   2. 当没有数据可读时，线程会主动睡眠释放 CPU，语义简单。
 *   3. 这种写法适合“线程数较少 + 连接数不高”的场景，是初学或快速验证的常用模式。
 */
void HandleClient(int conn_fd) {
    char buf[kBufSize];

    while (true) {
        /**
         * ::recv 在阻塞套接字上会遵循以下策略：
         *   - 如果 TCP 接收缓冲区没有数据，当前线程直接进入睡眠（阻塞态）。
         *     这样操作系统调度器可以把 CPU 让给其他就绪线程，减少忙轮询浪费。
         *   - 一旦有数据到达并被内核拷贝到缓冲区，内核唤醒线程，recv 返回实际字节数。
         *   - 由于这里没有复杂的事件循环，仅靠阻塞等待即可确保逻辑正确。
         */
        ssize_t n = ::recv(conn_fd, buf, sizeof(buf), 0);

        if (n > 0) {
            std::cout << "Received " << n << " bytes: "
                      << std::string(buf, buf + n) << std::endl;
        } else if (n == 0) {
            // 返回 0 说明对端已经有序关闭连接，我们也要关闭本端资源。
            std::cout << "Peer closed connection\n";
            break;
        } else {
            // 发生错误时打印 errno。阻塞模式下除 EINTR 外大多属致命错误。
            std::cerr << "recv error: " << std::strerror(errno) << std::endl;
            break;
        }
    }

    // 结束会话后关闭套接字，释放内核资源。
    ::close(conn_fd);
}

int main() {
    // 创建监听套接字。默认生成的文件描述符就是阻塞模式。
    int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        throw std::runtime_error("socket() failed");
    }

    sockaddr_in addr{};
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port        = htons(kListenPort);

    /**
     * bind + listen 建立阻塞监听套接字。
     * 为什么保持阻塞：
     *   - accept 在没有新连接时会自动睡眠，线程不会忙等。
     *   - 对于教学/演示用途，这种模式逻辑最直观，可以突出“阻塞 → 睡眠 → 唤醒”的状态转换。
     */
    if (::bind(listen_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
        ::close(listen_fd);
        throw std::runtime_error("bind() failed");
    }

    if (::listen(listen_fd, kBacklog) < 0) {
        ::close(listen_fd);
        throw std::runtime_error("listen() failed");
    }

    /**
     * 主线程循环等候新连接。
     * 阻塞式 accept 的好处：
     *   - 当没有客户端时，线程会睡眠等待，避免无意义的循环检查。
     *   - 代码路径简单，易于清晰展示传统“每连接一个线程”的架构。
     */
    while (true) {
        sockaddr_in client_addr{};
        socklen_t   client_len = sizeof(client_addr);

        int conn_fd = ::accept(listen_fd,
                               reinterpret_cast<sockaddr*>(&client_addr),
                               &client_len);
        if (conn_fd < 0) {
            std::cerr << "accept error: " << std::strerror(errno) << std::endl;
            continue;
        }

        /**
         * 典型的阻塞式服务器会为每个连接分配一个线程。
         * 这么做的原因：
         *   - 利用阻塞调用的简单语义：线程只需顺序调用 recv/send 即可。
         *   - 每个线程维护自己的调用栈和状态，逻辑隔离清晰。
         *   - 对于连接数有限、业务逻辑不复杂的场合，这种模式实现和维护成本低。
         * 缺点也在此显现：
         *   - 连接数与线程数成正比，线程栈、上下文切换开销高。
         *   - 在大规模并发时不具备扩展性，因此高并发服务器通常转向非阻塞 + 多路复用。
         */
        std::thread(HandleClient, conn_fd).detach();
    }

    ::close(listen_fd);
    return 0;
}